package com.glodon.paas.document.storage.util;

import java.io.IOException;
import java.lang.reflect.Field;
import java.security.MessageDigest;

import org.apache.commons.codec.binary.Hex;

public class DigestState {
    private static final String DIGEST = "SHA-1";
    private int[] state;
    private long processed;
    private byte[] buf;
    private int bufOfs;

    public DigestState(int[] state, long processed, byte[] buf, int bufOfs) {
        this.state = state;
        this.processed = processed;
        this.buf = buf;
        this.bufOfs = bufOfs;
    }

    public DigestState() {
    }

    public int[] getState() {
        return state;
    }

    public void setState(int[] state) {
        this.state = state;
    }

    public long getProcessed() {
        return processed;
    }

    public void setProcessed(long processed) {
        this.processed = processed;
    }

    public long getLength() {
        return processed + bufOfs;
    }

    public static DigestState getDigestState(String digestStateStr) throws IOException {
        try {
            byte[] bytes = Hex.decodeHex(digestStateStr.toCharArray());
            // length -> 8 byte, state -> 4 * size byte
            int[] state = new int[(bytes.length - 4 - 64 - 8) / 4];
            long length;
            for (int i = 0; i < state.length; i++) {
                state[i] = (bytes[i * 4] & 0xFF) << 24 | ((bytes[i * 4 + 1] & 0xFF) << 16)
                        | ((bytes[i * 4 + 2] & 0xFF) << 8) | (bytes[i * 4 + 3] & 0xFF);
            }
            int start = bytes.length - 4 - 64 - 8;
            length = ((bytes[start] & 0xFF) << 56) | ((bytes[start + 1] & 0xFF) << 48)
                    | ((bytes[start + 2] & 0xFF) << 40) | ((bytes[start + 3] & 0xFF) << 32)
                    | ((bytes[start + 4] & 0xFF) << 24) | ((bytes[start + 5] & 0xFF) << 16)
                    | ((bytes[start + 6] & 0xFF) << 8) | bytes[start + 7] & 0xFF;
            int bufStart = state.length * 4 + 8;
            byte[] buf = new byte[64];
            System.arraycopy(bytes, bufStart, buf, 0, 64);
            int bufOfsStart = bufStart + 64;
            int bufOfs = (bytes[bufOfsStart] & 0xFF) << 24 | ((bytes[bufOfsStart + 1] & 0xFF) << 16)
                    | ((bytes[bufOfsStart + 2] & 0xFF) << 8) | (bytes[bufOfsStart + 3] & 0xFF);
            return new DigestState(state, length, buf, bufOfs);
        } catch (Exception e) {
            throw new IOException("get message digest error", e);
        }
    }

    public MessageDigest getMessageDigest() throws IOException {
        try {
            MessageDigest md = createMessageDigest();
            if (state == null)
                return md;
            Field digestSpiField = md.getClass().getDeclaredField("digestSpi");
            digestSpiField.setAccessible(true);
            Object digestSpi = digestSpiField.get(md);
            Field stateField = digestSpi.getClass().getDeclaredField("state");
            stateField.setAccessible(true);
            stateField.set(digestSpi, state);
            Field bytesProcessedField = digestSpi.getClass().getSuperclass().getDeclaredField("bytesProcessed");
            bytesProcessedField.setAccessible(true);
            bytesProcessedField.set(digestSpi, processed);
            Field bufferField = digestSpi.getClass().getSuperclass().getDeclaredField("buffer");
            bufferField.setAccessible(true);
            bufferField.set(digestSpi, buf);
            Field bufOfsField = digestSpi.getClass().getSuperclass().getDeclaredField("bufOfs");
            bufOfsField.setAccessible(true);
            bufOfsField.setInt(digestSpi, bufOfs);
            return md;
        } catch (Exception e) {
            throw new IOException("get message digest error", e);
        }
    }

    public static DigestState getDigestState(MessageDigest md) throws IOException {
        try {
            Field digestSpiField = md.getClass().getDeclaredField("digestSpi");
            digestSpiField.setAccessible(true);
            Object digestSpi = digestSpiField.get(md);
            Field stateField = digestSpi.getClass().getDeclaredField("state");
            stateField.setAccessible(true);
            int[] state = (int[]) stateField.get(digestSpi);
            Field bytesProcessedField = digestSpi.getClass().getSuperclass().getDeclaredField("bytesProcessed");
            bytesProcessedField.setAccessible(true);
            Field bufOfsField = digestSpi.getClass().getSuperclass().getDeclaredField("bufOfs");
            bufOfsField.setAccessible(true);
            Field bufferField = digestSpi.getClass().getSuperclass().getDeclaredField("buffer");
            bufferField.setAccessible(true);
            byte[] buffer = (byte[]) bufferField.get(digestSpi);
            int bufOfs = bufOfsField.getInt(digestSpi);
            long processed = bytesProcessedField.getLong(digestSpi);
            return new DigestState(state.clone(), processed, buffer, bufOfs);
        } catch (Exception e) {
            throw new IOException("get digest state error", e);
        }
    }

    private static String toHex(int[] state, long length, byte[] buf, int bufOfs) {
        // int -> 4 bytes, long -> 8 bytes, buf 64 bytes, bufOfs int -> 4 bytes
        int bufStart = state.length * 4 + 8;
        byte[] bytes = new byte[bufStart + 64 + 4];
        int index = 0;
        for (int i = 0; i < state.length; i++) {
            int s = state[i];
            bytes[index++] = (byte) (s >> 24);
            bytes[index++] = (byte) (s >> 16);
            bytes[index++] = (byte) (s >> 8);
            bytes[index++] = (byte) (s);
        }
        bytes[index++] = (byte) (length >> 56);
        bytes[index++] = (byte) (length >> 48);
        bytes[index++] = (byte) (length >> 40);
        bytes[index++] = (byte) (length >> 32);
        bytes[index++] = (byte) (length >> 24);
        bytes[index++] = (byte) (length >> 16);
        bytes[index++] = (byte) (length >> 8);
        bytes[index++] = (byte) (length);
        System.arraycopy(buf, 0, bytes, bufStart, 64);
        index += 64;
        bytes[index++] = (byte) (bufOfs >> 24);
        bytes[index++] = (byte) (bufOfs >> 16);
        bytes[index++] = (byte) (bufOfs >> 8);
        bytes[index++] = (byte) (bufOfs);
        return Hex.encodeHexString(bytes);
    }

    @Override
    public String toString() {
        return toHex(state, processed, buf, bufOfs);
    }

    public static MessageDigest createMessageDigest() {
        try {
            return MessageDigest.getInstance(DIGEST);
        } catch (Exception e) {
            // ignore only
            throw new RuntimeException(e);
        }
    }
}
